/*
This file implements a websocket server that listens on port :12345/adb for
json requests that look like this:

  {
    'CreateCode': 'glClearColor(1, 1, 0, 1);',
    'DrawCode': 'glClear(GL_COLOR_BUFFER_BIT);'
  }

Every time such a request is received, the contained code is copied into
a template android project, which is then compiled and uploaded to and
executed on a device attached to the machine running this server.
The template program logs the current frames-per-second (FPS) it's running
at to the android log, where the server reads the FPS and sends them to the
websocket client with messages that look like this:

  {
    'Kind': 'fps',
    'Timestamp': 1232566,  // Some increasing value
    'Fps':, 55.0
  }

The server can also send messages with 'Kind' equal to 'info, 'error', and
'done'. In that case, the JSON contains a 'Text' field which contains
plaintext to be presented to the user somehow.

Run like this:
  6g server.go && 6l -o server server.6 && ./server
*/
package main

import (
	"bufio"
	"bytes"
	"exec"
	"fmt"
	"http"
	"io"
	"io/ioutil"
	"json"
	"log"
	"os"
	"path"
	"regexp"
	"strconv"
	"websocket"
)

const Adb = "/Users/thakis/src/android-sdk-mac_86/platform-tools/adb"
const Ant = "/usr/bin/ant"

////////////////////////////////////////////////////////////////////////////////
// Messages sent from client to server.

// Tells the server to build the project with |CreateCode| to be run at OpenGL
// context creation, and |DrawCode| on every draw.
type ClientCodeMessage struct {
	CreateCode string
	DrawCode   string
}

// Reads client messages from |reader| and sends them to |ch|. Since I/O can
// block, this runs in a goroutine. Closes |ch| and terminates when there's a
// reading error (e.g. EOF).
func parseMessages(reader io.Reader, ch chan ClientCodeMessage) {
	dec := json.NewDecoder(reader)
	for {
		var m ClientCodeMessage
		if err := dec.Decode(&m); err != nil {
			log.Println(err)
			close(ch)
			return
		}
		log.Printf("got create code: %s\n", m.CreateCode)
		log.Printf("got draw code: %s\n", m.DrawCode)
		ch <- m
	}
}


////////////////////////////////////////////////////////////////////////////////
// Messages sent from server to client.

// Informs the client of various events (compile started, etc).
type ServerStatusMessage struct {
	// This will be "info", "error", or "done".
	Kind string

	// This is free-form text.
	Text string
}

// Informs the client of the fps of the last draw call.
type ServerFpsMessage struct {
	Kind      string // Always "fps"
	Fps       float32
	Timestamp float64
}


////////////////////////////////////////////////////////////////////////////////

// Sent from the FPS-reading goroutine to the main thread every time an FPS
// result has been received from the device. The main thread has to send
// |true| on |StopChan| if it wants the FPS-reading goroutine to terminatej, or
// |false| when it's ready for the next FPS message.
type FpsResult struct {
	Fps       float32
	Timestamp float64
	StopChan  chan bool
}

// Reads log lines from |reader| and sends all the log lines that look like
// FPS results to |ch|. Every time a result has been sent, the function reads
// from |stop| to check if it should terminate. Since reading from a reader
// blocks, this function runs in a goroutine.
func sendlines(ch chan *FpsResult, reader io.Reader, stop chan bool) {
	bufferedReader := bufio.NewReader(reader)
	dd := `([0-9][0-9])`
	num := `([0-9]+\.[0-9]+)`
	fpsRegex := regexp.MustCompile(fmt.Sprintf(`%s-%s %s:%s:%s.*fps: %s`, dd, dd, dd, dd, num, num))
	for {
		logLine, err := bufferedReader.ReadString('\n')
		if err == os.EOF {
			log.Println("eof")
			return
		}
		matches := fpsRegex.FindStringSubmatch(string(logLine))
		if matches != nil {
			mon, _ := strconv.Atoi(matches[1])
			day, _ := strconv.Atoi(matches[2])
			hour, _ := strconv.Atoi(matches[3])
			min, _ := strconv.Atoi(matches[4])
			sec, _ := strconv.Atof64(matches[5])
			fps, _ := strconv.Atof32(matches[6])
			timestamp := sec + float64(60*(min+60*(hour+24*(day+30*mon))))
			//log.Printf("sending: %v", fps)
			ch <- &FpsResult{fps, timestamp, stop}
			shouldStop := <-stop
			if shouldStop {
				log.Println("logcat loop is terminating")
				return
			}
			//log.Printf("sent")
		}
	}
}

// Sends |text| as info message to the client, and logs it to the log.
func infolog(enc *json.Encoder, text string) {
	enc.Encode(ServerStatusMessage{Kind: "info", Text: text})
	log.Println(text)
}

// Sends |text| as error message to the client, and logs it to the log.
func errorlog(enc *json.Encoder, err os.Error) {
	enc.Encode(ServerStatusMessage{Kind: "error", Text: err.String()})
	log.Println(err)
}

// Given a client message |m|, this copies the source code from |m| into a
// template file, compiles the Android project with that source code injected,
// copies the resulting apk to a connected device, starts it, and returns a
// channel that reads from a goroutine which receives the current FPS the apk
// runs with on the device. |enc| is used to send log messages back to the
// client.
func handleMessage(m ClientCodeMessage, doneChan chan bool, enc *json.Encoder) chan *FpsResult {
	pwd, _ := os.Getwd()

	// 1. Paste string into template code file.
	template, err := ioutil.ReadFile(
		"IOOpenGLApp/src/com/example/io2011/IOOpenGLActivity.java.template")
	if err != nil {
		errorlog(enc, err)
		return nil
	}

	template = bytes.Replace(template, []byte("$INSERT_CREATE_CODE"), []byte(m.CreateCode), 1)
	template = bytes.Replace(template, []byte("$INSERT_CHANGE_CODE"), []byte(""), 1)
	template = bytes.Replace(template, []byte("$INSERT_DRAW_CODE"), []byte(m.DrawCode), 1)
	err = ioutil.WriteFile(
		"IOOpenGLApp/src/com/example/io2011/IOOpenGLActivity.java",
		template, 0666)
	if err != nil {
		errorlog(enc, err)
		return nil
	}

	// 2. Build apk by running |ant -f IOOpenGLApp/build.xml|.
	infolog(enc, "Compiling...")
	cmd, err := exec.Run(Ant, []string{"ant", "debug"},
		nil, path.Join(pwd, "IOOpenGLApp/"),
		exec.DevNull, exec.Pipe, exec.MergeWithStdout)
	bstr, _ := ioutil.ReadAll(cmd.Stdout)
	log.Printf("Output: %s\n", bstr)

	if err != nil {
		errorlog(enc, err)
		return nil
	}
	wait, err := cmd.Wait(0)
	if err != nil || wait.WaitStatus != 0 {
		errorlog(enc, err)
		return nil
	}
	cmd.Close()
	infolog(enc, "Compiled!")

	// 3. Install apk.
	infolog(enc, "Uploading...")
	cmd, err = exec.Run(Adb,
		[]string{"adb", "uninstall", "com.example.io2011"},
		nil, "", exec.DevNull, exec.DevNull, exec.DevNull)
	cmd.Close()

	cmd, err = exec.Run(Adb,
		[]string{"adb", "install", "IOOpenGLApp-debug.apk"},
		nil, path.Join(pwd, "IOOpenGLApp/bin"),
		exec.DevNull, exec.Pipe, exec.MergeWithStdout)
	bstr, _ = ioutil.ReadAll(cmd.Stdout)
	log.Printf("Output: %s\n", bstr)

	if err != nil {
		errorlog(enc, err)
		return nil
	}
	wait, err = cmd.Wait(0)
	if err != nil || wait.WaitStatus != 0 {
		errorlog(enc, err)
		return nil
	}
	cmd.Close()

	const Activity = "com.example.io2011/com.example.io2011.IOOpenGLActivity"
	cmd, err = exec.Run(Adb,
		[]string{"adb", "shell", "am start -n " + Activity},
		nil, "", exec.DevNull, exec.DevNull, exec.DevNull)
	cmd.Close()
	infolog(enc, "Uploaded!")

	// 4. While not done, read FPS numbers from |adb logcat| and send to client.
	cmd, err = exec.Run(Adb,
		[]string{"adb", "logcat", "-v", "time", "IO:D", "*:S"},
		nil, "", exec.DevNull, exec.Pipe, exec.MergeWithStdout)

	fpsChan := make(chan *FpsResult)
	go sendlines(fpsChan, cmd.Stdout, doneChan)

	enc.Encode(ServerStatusMessage{Kind: "done", Text: "Done"})

	return fpsChan
}

// Only one request at a time is supported, this is it.
var gCurrentRequest *websocket.Conn

// The handler function of the server.
func AdbServer(ws *websocket.Conn) {
	log.Println("request")
	gCurrentRequest = ws

	messageChan := make(chan ClientCodeMessage)
  var fpsChan chan *FpsResult
	doneChan := make(chan bool)

	enc := json.NewEncoder(ws)

	// Start the input parsing goroutine.
	go parseMessages(ws, messageChan)

	for ws == gCurrentRequest {
		select {
		case m := <-messageChan:
			if m.DrawCode == "" {
				// Client hung up.
				log.Println("Exiting, client hung up")
				return
			}
			doneChan = make(chan bool)
			fpsChan = handleMessage(m, doneChan, enc)
		case fps := <-fpsChan:
			//log.Printf("received fps: %v\n", fps.Fps)
			if fps.StopChan == doneChan {
				// Send fps back to client.
				enc.Encode(ServerFpsMessage{Kind: "fps", Fps: fps.Fps, Timestamp: fps.Timestamp})
				fps.StopChan <- false
			} else {
				// Cancel logcat when a new request comes in and logcat is running.
				fps.StopChan <- true
			}
		}
	}
	log.Println("Exiting, newer connection appeared")
}

func main() {
	log.Printf("up\n")
	http.Handle("/adb", websocket.Handler(AdbServer))
	err := http.ListenAndServe(":12345", nil)
	if err != nil {
		panic("ListenAndServe: " + err.String())
	}
}
